Introducción

José es un diseñador de juegos de mesa. Crea las reglas, diseña los gráficos, escoge su tema, número de jugadores y duración promedio del juego que tiene en mente. José es una persona tímida, y a pesar de que sus juegos suelen gustarle a sus amigos, él nunca ha querido publicarlos por miedo a que no sean bien recibidos. Se quiere demostrar a José, con una base de datos de calificaciones históricas de juegos de mesa, cómo hubieran sido recibidos sus juegos en promedio en la época que los fue creando.

Los datos a utilizar vienen de esta base de datos: (board_games)* que, en cambio, vienen de la página Board Game Geek.

Instalación de Paquetes

Procedemos para empezar en instalar los siguientes paquetes, se puede omitir este paso si ya se tienen previamente instalados. Aquí una lista de los cuales vamos a necesitar.

Cargar Librerías

Usando ‘library’ cargamos las librerías, con las cuales vas a hacer uso de las diferentes funciones.

library("data.table")
library("h2o")
library("ggplot2")
library("ggthemes")
library("data.tree")
library("tidyverse")
library("modeldata")
library("DataExplorer")
library("vtree")
library("caTools")
library("rpart")
library("rpart.plot")
library("lares")
library("tidymodels")
library("h2o")
library("caret")
library("doParallel")
library("caTools")

Ánalisis Descriptivo, Data Engineering

Leemos nuestro dataset

En este caso usamos read.csv. Procedemos a leer:

board_games <- read.csv("./board_games.csv") 

Observación de las primeras líneas

  • game_id Identificador único
  • description Descripción corta
  • image URL con imagen del juego
  • max_players Jugadores máximos
  • max_playtime Tiempo máximo de juego
  • min_age Edad mínima
  • min_players Jugadores mínimos
  • min_playtime Tiempo mínimo de juego
  • name Nombre del juego
  • playing_time Tiempo promedio de juego
  • thumbnail URL con thumbnail del juego
  • year_published Año de publicación
  • artist Diseñador gráfico del juego
  • category Categorías del juego (separadas por coma)
  • compilation Si es parte de una compilación, nombre de la compilación
  • designer Diseñador del juego
  • expansion Si hay una expansión, el nombre de la expansión
  • family Familia, equivalente a editora
  • mechanic Mecánicas, separadas por coma
  • publisher Compañía o persona que publicaron el juego (separadas por coma)
  • average_rating Calificación promedio en Board Game Geek
  • users_rated Número de usuarios que calificaron el juego
head(board_games)

Colnames de nuestro dataset

Después de una rápida observación, ejecutamos los siguientes comandos para confirmación:

colnames(board_games)
 [1] "game_id"        "description"    "image"          "max_players"    "max_playtime"   "min_age"        "min_players"    "min_playtime"   "name"          
[10] "playing_time"   "thumbnail"      "year_published" "artist"         "category"       "compilation"    "designer"       "expansion"      "family"        
[19] "mechanic"       "publisher"      "average_rating" "users_rated"   

Tipo de variables

Usando data explorer observamos el tipo de variables, casi tenemos el mismo porcentaje para las discretas y continua, y tenemos un bajo porcentaje de missing values:

  • Sólo el 0.99% de las filas están completas,
  • tenemos 11.54% de observaciones faltantes, es decir, dado que solo tenemos 0.99% de las filas completas, solo hay 10.55% de observaciones faltantes del total.

Estos valores faltantes nos podrán general problemas para analizar los datos, veamos un poco los perfiles que faltan.

plot_intro(board_games)

Missing plot

Para visualizar el perfil de los datos faltantes podemos utilizar la función plot_missing(). En la visualización debajo, podemos ver que la variables compilation y expansion, son las que les falta información, encontramos de que sólo el 2.63% (compilation), 16.54% (expansion) de nuestras filas estén completas y probablemente esta varible no sea de mucha infomación. Por tanto la podemos eliminar de nuestro dataframe, ahorita mismo!!

plot_missing(board_games)

Eliminamos la columna que tiene más missing values

Eliminamos compilation y expansion de nuestro dataframe:

final_board_games <- drop_columns(board_games, c("description", "image", "name", "thumbnail", "game_id", "compilation","expansion", "family", "artist", "mechanic"))
final_board_games <- drop_columns(final_board_games, c("designer", "publisher"))
colnames(final_board_games)
 [1] "max_players"    "max_playtime"   "min_age"        "min_players"    "min_playtime"   "playing_time"   "year_published" "category"       "average_rating"
[10] "users_rated"   
final_board_games <- na.omit(final_board_games) 

Ánalisis de Correlación

Podemos ver la más alta correlación en estas variables:

  • min_playtime-max_playtime
  • min_playtime-min_age
  • min_playtime-playing_time
  • average_rating-min_age
plot_correlation(na.omit(final_board_games), maxcat = 5L)
Ignored all discrete features since `maxcat` set to 5 categories!

Ahora de una manera más detallada vamos a analizar las variables más correlacionadas entre sí. El top 10:

corr_cross(final_board_games, # name of dataset
  max_pvalue = 0.05, # display only significant correlations (at 5% level)
  top = 10 # display top 10 couples of variables (by correlation coefficient)
)
Returning only the top 10. You may override with the 'top' argument

QQ plot

La gráfica Quantile-Quantile es una forma de visualizar la desvisión de una distribución de probabilidad específica.

Después de analizar estos gráficos, a menudo es beneficioso aplicar una transformación matemática (como logaritmo) para modelos como la regresión lineal. Para hacerlo, podemos usar la función plot_qq. De forma predeterminada, se compara con la distribución normal.

qq_data <- final_board_games[, c("min_playtime", "max_playtime", "min_age", "playing_time", "average_rating")]

plot_qq(qq_data, sampled_rows = 1000L)

En el gráfico, las columnas parecen sesgadas en ambas colas. Apliquemos una transformación logarítmica simple y grafiquemos de nuevo.

log_qq_data <- update_columns(qq_data, 1:5, function(x) log(x + 1))


plot_qq(log_qq_data, sampled_rows = 1000L)

Ánalisis Exploratorio de los Datos

Teniendo nuestras variables con mayor correlación vamos a graficarlas con geom point..:

  • min_playtime-min_age
final_board_games %>%  ggplot(aes(x = min_playtime, y = min_age)) + 
  geom_point()

  • average_rating-min_age
final_board_games %>%  ggplot(aes(x = average_rating, y = min_age)) + 
  geom_point()

  • average_rating-playing_time
final_board_games %>%  ggplot(aes(x = playing_time, y = average_rating)) + 
  geom_point()

  • users_rated-average_rating
final_board_games %>%  ggplot(aes(x = users_rated, y = average_rating)) + 
  geom_point()

###Using vtree para explorar

Usamos vtree para observar la concentración de los datos por ejemplo para min_age, donde la mayoría de los datos se concentran en min_age de 8 años, 10 años y 12 años.

vtree(final_board_games, "min_age")

Usamos vtree para observar la concentración de los datos por ejemplo para min_players, tenemos casi un 69% para min 2 jugadores y cerca del 19% para min 3 jugadores.

vtree(final_board_games, "min_players")

Usamos vtree para observar la concentración de los datos por ejemplo para max_players, tenemos casi un 23% para máx 4 jugadores y cerca del 25% para máx 6 jugadores.

vtree(final_board_games, "max_players")

¿Que se ha hecho hasta ahora?

Se realizó una exploración de datos, donde primero eliminalos columnas que no tienen mucha significancia en la predicción de nuestra variable de calificación. Después vimos su correlación entre las existentes.

Se tiene más claro cuales son las variables más significativas a la predicción, se hizo una limpieza, tenemos datos más contundentes con los cuales comenzar nuestra predicción, menos outliers sobre todo.

Propuestas

Debido a que el problema intenta convencer a José de que sus juegos pudieron haber sido (en promedio) bien recibidos, y de cómo se espera que se reciban en un futuro, la variable de salida de nuestro problema es la calificación de los usuarios del sitio web. Esto puede hacerse de dos maneras: una regresión y tomar la calificación como una variable continua, o redondear y tomarlo como problema de clasificación (calificación discreta de 0 a 10). Las propuestas para estos casos son

Regresión

  • Support Vector Regression
  • Random Forest
  • Regresión lineal múltiple

Clasificación

  • Support Vector Machine
  • Random Forest
  • Multilayer perceptron

Vamos a suponer que a la comunidad de juegos de mesa no les importa tanto el historial del autor del juego ni quién lo publique, por lo que esas columnas se eliminarían del análisis. Si José ve que sus juegos no hubieran gustado, al menos podrá tener un modelo con el cuál puede saber qué es lo que suele gustarle a la gente, por lo que podría hacer investigación de seguimiento para entablar las causas raíces.

Modelado

Primero hacemos la separación de los datos en train y test. Todos los modelos usarán los mismos subconjuntos para poder evaluarlos y compararlos en un terreno nivelado.

library(caTools)
set.seed(0)
split = sample.split(final_board_games, SplitRatio=0.6)
data.train = subset(final_board_games, split=TRUE)
data.test = subset(final_board_games, split=FALSE)

Support Vector Regression

library(caret)
library(doParallel)
set.seed(0)
control = trainControl(method="repeatedcv", repeats=5, search="random")
registerDoParallel(cores = parallel::detectCores() - 1)
model.svr = train(average_rating ~ ., data = drop_columns(data.train, "category"),
               method = "svmRadial",
               tuneLength = 15,
               metric = "RMSE",
               preProc = c("center", "scale"),
               trControl = control)
model.svr
Support Vector Machines with Radial Basis Function Kernel 

1200 samples
   8 predictor

Pre-processing: centered (8), scaled (8) 
Resampling: Cross-Validated (10 fold, repeated 5 times) 
Summary of sample sizes: 1080, 1080, 1080, 1080, 1080, 1080, ... 
Resampling results across tuning parameters:

  sigma       C             RMSE       Rsquared    MAE      
  0.01226831   45.77096245  0.5971698  0.28603519  0.4569200
  0.01450086  432.22566749  0.6077075  0.27828915  0.4600209
  0.01733709    0.08424009  0.6385926  0.22166352  0.4933061
  0.01797172    0.68418148  0.6089168  0.26137934  0.4676323
  0.01949306   53.19382628  0.5923793  0.29802023  0.4532379
  0.03197837   97.46893089  0.6050052  0.27990795  0.4595333
  0.04891373    0.09331815  0.6188100  0.25343570  0.4756074
 0.08517855   1.13103822 0.5900931 0.30271546 0.4517433
 0.09135066  15.15147202 0.5944384 0.29497671 0.4532882
 0.45152266 986.00398192 1.0261372 0.09002466 0.6915994
 0.61653904   0.15882372 0.6108620 0.26636552 0.4694599
 1.29998370  10.85267954 0.6525816 0.20588346 0.5030971
 1.34663993   0.16233196 0.6224801 0.24125207 0.4783713
 1.35594685   0.51325259 0.6090114 0.25717566 0.4658292
 4.87616608   0.47422243 0.6315189 0.20311225 0.4874107

RMSE was used to select the optimal model using the smallest value.
The final values used for the model were sigma = 0.08517855 and C = 1.131038.
plot_qq(predict(model.svr, newdata=data.test) - data.test$average_rating)

H2O Models

Inicializar H2O

Creamos el clusgter local con todos los cores disponibles de la siguiente forma: Se eliminan los datos del cluster por si ya había sido inicializado. Tras iniciar el cluster (local), se muestran por pantalla sus características, entre las que están: el número de cores activados (4), la memoria total del cluster (5.32 GB), el número de nodos (1 porque se está empleando un único computador) y el puerto con el que conectarse a la interfaz web de H2O (http://localhost:54321/flow/index.html).

# inicialización de h2o
h2o.init(
  ip = "localhost",
  # -1 indica que se empleen todos los cores disponibles.
  nthreads = -1,
  # Máxima memoria disponible para el cluster.
  max_mem_size = "6g"
)
 Connection successful!

R is connected to the H2O cluster: 
    H2O cluster uptime:         1 minutes 44 seconds 
    H2O cluster timezone:       America/Mexico_City 
    H2O data parsing timezone:  UTC 
    H2O cluster version:        3.32.1.3 
    H2O cluster version age:    1 month and 23 days  
    H2O cluster name:           H2O_started_from_R_Gabo_ljy373 
    H2O cluster total nodes:    1 
    H2O cluster total memory:   6.00 GB 
    H2O cluster total cores:    12 
    H2O cluster allowed cores:  12 
    H2O cluster healthy:        TRUE 
    H2O Connection ip:          localhost 
    H2O Connection port:        54321 
    H2O Connection proxy:       NA 
    H2O Internal Security:      FALSE 
    H2O API Extensions:         Amazon S3, Algos, AutoML, Core V3, TargetEncoder, Core V4 
    R Version:                  R version 4.1.0 (2021-05-18) 
h2o.removeAll()
h2o.no_progress()

Carga de datos -Separación de training, validación y test

La carga de datos puede hacerse directamente al cluster H2O, o bien cargándolos primero en memoria en la sesión de R y después transfiriéndolos. La segunda opción no es aconsejable si el volumen de datos es muy grande.

Para nuestro caso el conjunto de datos de turbines es suficientemente pequeño y lo podemos almacenar en memoria, por tanto lo podemos llamar con la siguiente función.

Antes de hacer la separación tengamos claro la diferencia entre estas particiones del conjunto de datos:

Datos de train: la muestra de los datos utilizada para ajustar el modelo.

Datos de validación: la muestra de datos que se utiliza para proporcionar una evaluación imparcial de un ajuste de modelo en el conjunto de datos de train mientras se ajustan los hiperparámetros del modelo. La evaluación se vuelve más sesgada a medida que la habilidad del conjunto de datos de validación se incorpora a la configuración del modelo.

Datos de test: la muestra de datos utilizada para proporcionar una evaluación imparcial de un ajuste final del modelo en el conjunto de datos de entrenamiento.

La función h2o.splitFrame() realiza particiones aleatorias, pero no permite hacerlas de forma estratificada, por lo que no asegura que la distribución de clases de variable respuesta sea igual en todas particiones. Esto puede ser problemático con datos muy desbalanceados (alguno de los grupos es muy minoritario).

En el momento en que consideremos la validación, debemos agregar en los ratios el porcentaje de la validación, en este caso será train (60%), validación (20%) y test (20%). En la semilla se le agrega el el numeral 4 y se adiciona un nuevo subconjunto de datos, entendiendo que el 1 es train, el 2 es validación y el 3 es test.

datos_h2o <- as.h2o(x = final_board_games, destination_frame = "datos_h2o")

datos_train_h2o <- as.h2o(x = data.train, key = "datos_train_h2o")
datos_valid_h2o <- as.h2o(x = data.test, key = "datos_valid_h2O")

Random Forest

La función para este modelo en h2o es h2o.randomForest. Dentro de ella debemos de especificar los datos de train que convertimos dentro de h2o y, si así lo queremos los datos de validación. Para cuando no queremos utilizar datos de validación esta línea se omite dentro del modelo cambia la partición del conjunto de datos. Se descartan las columnas categóricas , usamos solo las númericas para este random forest, también quitamos el object_id, solo nos interesa el rango x = c(1, 2, 3, 4, 5, 6, 7, 8, 10), y sy predicción que es la y = 9.

model.h2o.rf = h2o.randomForest(
  training_frame = datos_train_h2o,
  validation_frame = datos_valid_h2o,
  x = c(1, 2, 3, 4, 5, 6, 7, 8, 10),
  y = 9,
  model_id = "rf_covType_v1",
  ntrees = 200,
  stopping_rounds = 2,
  score_each_iteration = T,
  seed = 26
)
Dropping bad and constant columns: [category].
summary(model.h2o.rf)
Model Details:
==============

H2ORegressionModel: drf
Model Key:  rf_covType_v1 
Model Summary: 

H2ORegressionMetrics: drf
** Reported on training data. **
** Metrics reported on Out-Of-Bag training samples **

MSE:  0.3763041
RMSE:  0.6134363
MAE:  0.4666793
RMSLE:  0.08950471
Mean Residual Deviance :  0.3763041


H2ORegressionMetrics: drf
** Reported on validation data. **

MSE:  0.07822108
RMSE:  0.2796803
MAE:  0.2091356
RMSLE:  0.04187238
Mean Residual Deviance :  0.07822108




Scoring History: 

---

Variable Importances: (Extract with `h2o.varimp`) 
=================================================

Variable Importances: 

Gradient Boosting Machines (GBM)

Primero haremos todas la configuraciones predeterminadas y luego comenzaremos a hacer algunos cambios donde se describen los parámetros y los valores predeterminados.

Podemos observar una estructura muy similar a la del random forest, ahora utilizaremos la función h2o.gbm.. NOTA: En la mayoría de los algorimos el primero es para regresión y el segundo para clasificación.

gbm_model <- h2o.gbm(
  training_frame = datos_train_h2o, # datos de h2o para training
  validation_frame = datos_valid_h2o, # datos de h2o para validación (no es requerido)
  x = c(1, 2, 3, 4, 5, 6, 7, 8, 10),, # Las columnas predictoras, por índice
  y = 9,    # La columna que queremos predecir, variable objetivo
  model_id = "gbm_covType1", # nombre del modelo en h2o
  seed = 2000000   # Establecer una semilla aleatoria para que se pueda reproducir
) 
Dropping bad and constant columns: [category].
summary(gbm_model)
Model Details:
==============

H2ORegressionModel: gbm
Model Key:  gbm_covType1 
Model Summary: 

H2ORegressionMetrics: gbm
** Reported on training data. **

MSE:  0.2185903
RMSE:  0.4675364
MAE:  0.3519328
RMSLE:  0.06912309
Mean Residual Deviance :  0.2185903


H2ORegressionMetrics: gbm
** Reported on validation data. **

MSE:  0.2185903
RMSE:  0.4675364
MAE:  0.3519327
RMSLE:  0.0691231
Mean Residual Deviance :  0.2185903




Scoring History: 

---

Variable Importances: (Extract with `h2o.varimp`) 
=================================================

Variable Importances: 

Scoring del modelo

Podemos ver la evolución del modelo, para evaluar cómo aprende el modelo a medida que se añaden nuevos árboles al ensamble.

h2o almacena las métricas de entrenamiento y test bajo el nombre de scoring. Los valores se encuentran almacenados dentro del modelo.

scoring <- as.data.frame(gbm_model@model$scoring_history)
head(scoring)

Importancia Variables del modelo

En los modelos GBM, se puede estudiar la influencia de los predictores cuantificando la reducción total de error cuadrático que ha conseguido cada predictor en el conjunto de todos los árboles que forman el modelo.

importancia <- as.data.frame(gbm_model@model$variable_importances)
importancia

ggplot variables importancia del modelo

ggplot(data = importancia,
       aes(x = reorder(variable, scaled_importance), y = scaled_importance)) +
  geom_col() +
  coord_flip() +
  labs(title = "Importancia de los predictores en el modelo GBM",
       subtitle = "Importancia en base a la reducción del error cuadrático medio",
       x = "Predictor",
       y = "Importancia relativa") +
  theme_bw()

Modelo GBM alternativo

En los modelos GBM, se puede estudiar la influencia de los predictores cuantificando la reducción total de error cuadrático que ha conseguido cada predictor en el conjunto de todos los árboles que forman el modelo.

gbm_model_2 <- h2o.gbm(
  training_frame = datos_train_h2o, # datos de h2o para training
  validation_frame = datos_valid_h2o, # datos de h2o para validación (no es requerido)
  x = c(2:3,5:11), # Las columnas predictoras, por índice
  y = 4,    # La columna que queremos predecir, variable objetivo
  model_id = "gbm_covType1", # nombre del modelo en h2o
  ntrees = 200, 
  max_depth = 30,
  stopping_rounds = 2,
  stopping_tolerance = 1e-2,
  seed = 2000000   # Establecer una semilla aleatoria para que se pueda reproducir
) 
Dropping bad and constant columns: [category].
early stopping is enabled but neither score_tree_interval or score_each_iteration are defined. Early stopping will not be reproducible!.

Métricas

gbm_model_2@model$validation_metrics
H2ORegressionMetrics: gbm
** Reported on validation data. **

MSE:  0.007926617
RMSE:  0.08903155
MAE:  0.05606627
RMSLE:  0.03122496
Mean Residual Deviance :  0.007926617

Predicciones y error

Una vez hemos ajustado el modelo, se puede predecir nuevas observaciones y estimar el error de test.

# Predictores para el modelo de random forest
predicciones <- h2o.predict(
  object = model.h2o.rf,
  newdata = datos_valid_h2o
)
head(predicciones)
# Predictores para el modelo de GBM
predicciones_2 <- h2o.predict(
  object = gbm_model,
  newdata = datos_valid_h2o
)
head(predicciones_2)

Comparasión

En total se tuvieron 4 modelos: Support Vector Regression, Random Forest, Gradient Boosting Machine y un GBM alternativo. Revisemos sus errores de entrenamiento y de prueba.

svr.rmse.train = min(model.svr$results$RMSE)
svr.rmse.test = ModelMetrics::rmse(predict(model.svr, newdata=data.test), data.test$average_rating)

rf.rmse.train = tail(model.h2o.rf@model$scoring_history$training_rmse, 1)
rf.rmse.test = tail(model.h2o.rf@model$scoring_history$validation_rmse, 1)

gbm1.rmse.train = tail(gbm_model@model$scoring_history$training_rmse, 1)
gbm1.rmse.test = tail(gbm_model@model$scoring_history$validation_rmse, 1)

gbm2.rmse.train = tail(gbm_model_2@model$scoring_history$training_rmse, 1)
gbm2.rmse.test = tail(gbm_model_2@model$scoring_history$validation_rmse, 1)

De izquierda a derecha: RMSE de entrenamiento de SVR, RF, GBM1 y GBM2

barplot(c(svr.rmse.train, rf.rmse.train, gbm1.rmse.train, gbm2.rmse.train))

De izquierda a derecha: RMSE de validación de SVR, RF, GBM1 y GBM2

barplot(c(svr.rmse.test, rf.rmse.test, gbm1.rmse.test, gbm2.rmse.test))

Recordemos que todos los modelos usan el mismo subconjunto de entrenamiento y de validación, y todos presentan una retroalimentación para la optimización de hiperparámetros. Parece que el mejor modelo es el GBM2, pues tiene un error de entrenamiento y de validación mucho más bajos que los otros.

Conclusiones

La exploración de datos es una fase muy importante en el ciclo de vida de un proyecto de ciencia de datos. El entender la distribución de las variables te da una idea mucho más clara de qué es lo que podrías usar para predecir la salida que se necesita; aunque el entendimiento del negocio es una fase que puede tomar un tiempo más largo (nosotros tuvimos la suerte de que ya entendíamos cómo funcionaba el sitio web en el que se basa el conjunto de datos que usamos).

Las mil y una formas de implementar un modelo predictivo también se convierten en una barrera para seguir el proyecto: ¿cuál de todas las opciones es la mejor para el problema que se tiene? ¿Cómo justificas usar un Random Forest contra una red neuronal? (Seguramente con práctica y pericia).

Referencias

LS0tDQp0aXRsZTogIkJvYXJkX0dhbWVzX1JlZ3Jlc3Npb25fUHJvamVjdCINCmF1dGhvcjogJ0FkcmlhbiBIb21lcm8gTW9yZW5vIEdhcmPDrWEtIGFkcmlhbi5tb3Jlbm9AaXRlc28ubXgsIEdhYnJpZWwgQWxlamFuZHJvIE1vcmFsZXMNCiAgUnVpei0gaWU2OTM4NzFAaXRlc28ubXgnDQpkYXRlOiAiNi8yMS8yMDIxIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50Og0KICAgIHRvYzogeWVzDQogICAgZGZfcHJpbnQ6IHBhZ2VkDQogIGdpdGh1Yl9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KICAgIGRldjoganBlZw0KICBodG1sX25vdGVib29rOg0KICAgIHRvYzogeWVzDQogICAgdG9jX2Zsb2F0OiB5ZXMNCiAgICB0aGVtZTogY29zbW8NCiAgICBoaWdobGlnaHQ6IHRhbmdvDQotLS0NCg0KYGBge3Igc2V0dXAsIGVjaG8gPSBGQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgIGZpZy5oZWlnaHQgPSA2LCBmaWcud2lkdGggPSA3KQ0KYGBgDQoNCjxzdHlsZT4NCi5mb3JjZUJyZWFrIHsgLXdlYmtpdC1jb2x1bW4tYnJlYWstYWZ0ZXI6IGFsd2F5czsgYnJlYWstYWZ0ZXI6IGNvbHVtbjsgfQ0KPC9zdHlsZT4NCg0KPGNlbnRlcj4NCiFbXSguL2ltYWdlcy9pdGVzby5qcGVnKXt3aWR0aD0yMCV9DQoNCg0KPC9jZW50ZXI+DQoNCiMgSW50cm9kdWNjacOzbg0KDQpKb3PDqSBlcyB1biBkaXNlw7FhZG9yIGRlIGp1ZWdvcyBkZSBtZXNhLiBDcmVhIGxhcyByZWdsYXMsIGRpc2XDsWEgbG9zIGdyw6FmaWNvcywgZXNjb2dlIHN1IHRlbWEsIG7Dum1lcm8gZGUganVnYWRvcmVzIHkgZHVyYWNpw7NuIHByb21lZGlvIGRlbCBqdWVnbyBxdWUgdGllbmUgZW4gbWVudGUuIEpvc8OpIGVzIHVuYSBwZXJzb25hIHTDrW1pZGEsIHkgYSBwZXNhciBkZSBxdWUgc3VzIGp1ZWdvcyBzdWVsZW4gZ3VzdGFybGUgYSBzdXMgYW1pZ29zLCDDqWwgbnVuY2EgaGEgcXVlcmlkbyBwdWJsaWNhcmxvcyBwb3IgbWllZG8gYSBxdWUgbm8gc2VhbiBiaWVuIHJlY2liaWRvcy4gU2UgcXVpZXJlIGRlbW9zdHJhciBhIEpvc8OpLCBjb24gdW5hIGJhc2UgZGUgZGF0b3MgZGUgY2FsaWZpY2FjaW9uZXMgaGlzdMOzcmljYXMgZGUganVlZ29zIGRlIG1lc2EsIGPDs21vIGh1YmllcmFuIHNpZG8gcmVjaWJpZG9zIHN1cyBqdWVnb3MgZW4gcHJvbWVkaW8gZW4gbGEgw6lwb2NhIHF1ZSBsb3MgZnVlIGNyZWFuZG8uDQoNCkxvcyBkYXRvcyBhIHV0aWxpemFyIHZpZW5lbiBkZSBlc3RhIGJhc2UgZGUgZGF0b3M6IA0KWyhib2FyZF9nYW1lcyldKGh0dHBzOi8vZ2l0aHViLmNvbS9yZm9yZGF0YXNjaWVuY2UvdGlkeXR1ZXNkYXkvdHJlZS9tYXN0ZXIvZGF0YS8yMDE5LzIwMTktMDMtMTIpKg0KcXVlLCBlbiBjYW1iaW8sIHZpZW5lbiBkZSBsYSBww6FnaW5hIEJvYXJkIEdhbWUgR2Vlay4NCg0KIyMgSW5zdGFsYWNpw7NuIGRlIFBhcXVldGVzDQoNClByb2NlZGVtb3MgcGFyYSBlbXBlemFyIGVuIGluc3RhbGFyIGxvcyBzaWd1aWVudGVzIHBhcXVldGVzLCBzZSBwdWVkZSBvbWl0aXIgZXN0ZSBwYXNvIHNpIHlhIHNlIHRpZW5lbiBwcmV2aWFtZW50ZSBpbnN0YWxhZG9zLiBBcXXDrSB1bmEgbGlzdGEgZGUgbG9zIGN1YWxlcyB2YW1vcyBhIG5lY2VzaXRhci4NCg0KYGBge3J9DQojaW5zdGFsbC5wYWNrYWdlcygiZGF0YS50YWJsZSIpDQojaW5zdGFsbC5wYWNrYWdlcygiaDJvIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJnZ3Bsb3QyIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJnZ3RoZW1lcyIpDQojaW5zdGFsbC5wYWNrYWdlcygiZGF0YS50cmVlIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJ0aWR5dmVyc2UiKQ0KI2luc3RhbGwucGFja2FnZXMoIm1vZGVsZGF0YSIpDQojaW5zdGFsbC5wYWNrYWdlcygiRGF0YUV4cGxvcmVyIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJ2dHJlZSIpDQojaW5zdGFsbC5wYWNrYWdlcygiY2FUb29scyIpDQojaW5zdGFsbC5wYWNrYWdlcygicnBhcnQiKQ0KI2luc3RhbGwucGFja2FnZXMoInJwYXJ0LnBsb3QiKQ0KI2luc3RhbGwucGFja2FnZXMoImxhcmVzIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJ0aWR5bW9kZWxzIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJoMm8iKQ0KI2luc3RhbGwucGFja2FnZXMoImNhcmV0IikNCiNpbnN0YWxsLnBhY2thZ2VzKCJkb1BhcmFsbGVsIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJjYVRvb2xzIikNCmBgYA0KIyMgQ2FyZ2FyIExpYnJlcsOtYXMNCiANClVzYW5kbyAnbGlicmFyeScgY2FyZ2Ftb3MgbGFzIGxpYnJlcsOtYXMsIGNvbiBsYXMgY3VhbGVzIHZhcyBhIGhhY2VyIHVzbyBkZSBsYXMgZGlmZXJlbnRlcyBmdW5jaW9uZXMuIA0KDQpgYGB7cn0NCmxpYnJhcnkoImRhdGEudGFibGUiKQ0KbGlicmFyeSgiaDJvIikNCmxpYnJhcnkoImdncGxvdDIiKQ0KbGlicmFyeSgiZ2d0aGVtZXMiKQ0KbGlicmFyeSgiZGF0YS50cmVlIikNCmxpYnJhcnkoInRpZHl2ZXJzZSIpDQpsaWJyYXJ5KCJtb2RlbGRhdGEiKQ0KbGlicmFyeSgiRGF0YUV4cGxvcmVyIikNCmxpYnJhcnkoInZ0cmVlIikNCmxpYnJhcnkoImNhVG9vbHMiKQ0KbGlicmFyeSgicnBhcnQiKQ0KbGlicmFyeSgicnBhcnQucGxvdCIpDQpsaWJyYXJ5KCJsYXJlcyIpDQpsaWJyYXJ5KCJ0aWR5bW9kZWxzIikNCmxpYnJhcnkoImgybyIpDQpsaWJyYXJ5KCJjYXJldCIpDQpsaWJyYXJ5KCJkb1BhcmFsbGVsIikNCmxpYnJhcnkoImNhVG9vbHMiKQ0KYGBgDQoNCiMjIMOBbmFsaXNpcyBEZXNjcmlwdGl2bywgRGF0YSBFbmdpbmVlcmluZw0KIA0KIyMjIExlZW1vcyBudWVzdHJvIGRhdGFzZXQNCg0KRW4gZXN0ZSBjYXNvIHVzYW1vcyByZWFkLmNzdi4gUHJvY2VkZW1vcyBhIGxlZXI6DQoNCmBgYHtyfQ0KYm9hcmRfZ2FtZXMgPC0gcmVhZC5jc3YoIi4vYm9hcmRfZ2FtZXMuY3N2IikgDQpgYGANCg0KIyMjIE9ic2VydmFjacOzbiBkZSBsYXMgcHJpbWVyYXMgbMOtbmVhcw0KDQotIGdhbWVfaWQJSWRlbnRpZmljYWRvciDDum5pY28NCi0gZGVzY3JpcHRpb24JRGVzY3JpcGNpw7NuIGNvcnRhDQotIGltYWdlCVVSTCBjb24gaW1hZ2VuIGRlbCBqdWVnbw0KLSBtYXhfcGxheWVycwlKdWdhZG9yZXMgbcOheGltb3MNCi0gbWF4X3BsYXl0aW1lCVRpZW1wbyBtw6F4aW1vIGRlIGp1ZWdvDQotIG1pbl9hZ2UJRWRhZCBtw61uaW1hDQotIG1pbl9wbGF5ZXJzCUp1Z2Fkb3JlcyBtw61uaW1vcw0KLSBtaW5fcGxheXRpbWUJVGllbXBvIG3DrW5pbW8gZGUganVlZ28NCi0gbmFtZQlOb21icmUgZGVsIGp1ZWdvDQotIHBsYXlpbmdfdGltZQlUaWVtcG8gcHJvbWVkaW8gZGUganVlZ28NCi0gdGh1bWJuYWlsCVVSTCBjb24gdGh1bWJuYWlsIGRlbCBqdWVnbw0KLSB5ZWFyX3B1Ymxpc2hlZAlBw7FvIGRlIHB1YmxpY2FjacOzbg0KLSBhcnRpc3QJRGlzZcOxYWRvciBncsOhZmljbyBkZWwganVlZ28NCi0gY2F0ZWdvcnkJQ2F0ZWdvcsOtYXMgZGVsIGp1ZWdvIChzZXBhcmFkYXMgcG9yIGNvbWEpDQotIGNvbXBpbGF0aW9uCVNpIGVzIHBhcnRlIGRlIHVuYSBjb21waWxhY2nDs24sIG5vbWJyZSBkZSBsYSBjb21waWxhY2nDs24NCi0gZGVzaWduZXIJRGlzZcOxYWRvciBkZWwganVlZ28NCi0gZXhwYW5zaW9uCVNpIGhheSB1bmEgZXhwYW5zacOzbiwgZWwgbm9tYnJlIGRlIGxhIGV4cGFuc2nDs24NCi0gZmFtaWx5CUZhbWlsaWEsIGVxdWl2YWxlbnRlIGEgZWRpdG9yYQ0KLSBtZWNoYW5pYwlNZWPDoW5pY2FzLCBzZXBhcmFkYXMgcG9yIGNvbWENCi0gcHVibGlzaGVyCUNvbXBhw7HDrWEgbyBwZXJzb25hIHF1ZSBwdWJsaWNhcm9uIGVsIGp1ZWdvIChzZXBhcmFkYXMgcG9yIGNvbWEpDQotIGF2ZXJhZ2VfcmF0aW5nCUNhbGlmaWNhY2nDs24gcHJvbWVkaW8gZW4gQm9hcmQgR2FtZSBHZWVrDQotIHVzZXJzX3JhdGVkCU7Dum1lcm8gZGUgdXN1YXJpb3MgcXVlIGNhbGlmaWNhcm9uIGVsIGp1ZWdvDQoNCmBgYHtyfQ0KaGVhZChib2FyZF9nYW1lcykNCmBgYA0KDQojIyMgQ29sbmFtZXMgZGUgbnVlc3RybyBkYXRhc2V0DQoNCkRlc3B1w6lzIGRlIHVuYSByw6FwaWRhIG9ic2VydmFjacOzbiwgZWplY3V0YW1vcyBsb3Mgc2lndWllbnRlcyBjb21hbmRvcyBwYXJhIGNvbmZpcm1hY2nDs246DQoNCmBgYHtyfQ0KY29sbmFtZXMoYm9hcmRfZ2FtZXMpDQpgYGANCg0KIyMjIFRpcG8gZGUgdmFyaWFibGVzDQoNClVzYW5kbyBkYXRhIGV4cGxvcmVyIG9ic2VydmFtb3MgZWwgdGlwbyBkZSB2YXJpYWJsZXMsIGNhc2kgdGVuZW1vcyBlbCBtaXNtbyBwb3JjZW50YWplIHBhcmEgbGFzIGRpc2NyZXRhcyB5IGNvbnRpbnVhLCB5IHRlbmVtb3MgdW4gYmFqbyBwb3JjZW50YWplIGRlIG1pc3NpbmcgdmFsdWVzOg0KDQotIFPDs2xvIGVsIDAuOTklIGRlIGxhcyBmaWxhcyBlc3TDoW4gY29tcGxldGFzLA0KLSB0ZW5lbW9zIDExLjU0JSBkZSBvYnNlcnZhY2lvbmVzIGZhbHRhbnRlcywgZXMgZGVjaXIsIGRhZG8gcXVlIHNvbG8gdGVuZW1vcyAwLjk5JSBkZSBsYXMgZmlsYXMgY29tcGxldGFzLCBzb2xvIGhheSAxMC41NSUgZGUgb2JzZXJ2YWNpb25lcyBmYWx0YW50ZXMgZGVsIHRvdGFsLg0KDQpFc3RvcyB2YWxvcmVzIGZhbHRhbnRlcyBub3MgcG9kcsOhbiBnZW5lcmFsIHByb2JsZW1hcyBwYXJhIGFuYWxpemFyIGxvcyBkYXRvcywgdmVhbW9zIHVuIHBvY28gbG9zIHBlcmZpbGVzIHF1ZSBmYWx0YW4uDQoNCmBgYHtyIGJhcnBsb3R9DQpwbG90X2ludHJvKGJvYXJkX2dhbWVzKQ0KYGBgDQoNCiMjIyBNaXNzaW5nIHBsb3QNCg0KUGFyYSB2aXN1YWxpemFyIGVsIHBlcmZpbCBkZSBsb3MgZGF0b3MgZmFsdGFudGVzIHBvZGVtb3MgdXRpbGl6YXIgbGEgZnVuY2nDs24gcGxvdF9taXNzaW5nKCkuIEVuIGxhIHZpc3VhbGl6YWNpw7NuIGRlYmFqbywgcG9kZW1vcyB2ZXIgcXVlIGxhIHZhcmlhYmxlcyBjb21waWxhdGlvbiB5IGV4cGFuc2lvbiwgc29uIGxhcyBxdWUgbGVzIGZhbHRhIGluZm9ybWFjacOzbiwgZW5jb250cmFtb3MgZGUgcXVlIHPDs2xvIGVsIDIuNjMlIChjb21waWxhdGlvbiksIDE2LjU0JSAoZXhwYW5zaW9uKSBkZSBudWVzdHJhcyBmaWxhcyBlc3TDqW4gY29tcGxldGFzIHkgcHJvYmFibGVtZW50ZSBlc3RhIHZhcmlibGUgbm8gc2VhIGRlIG11Y2hhIGluZm9tYWNpw7NuLiBQb3IgdGFudG8gbGEgcG9kZW1vcyBlbGltaW5hciBkZSBudWVzdHJvIGRhdGFmcmFtZSwgYWhvcml0YSBtaXNtbyEhDQoNCmBgYHtyfQ0KcGxvdF9taXNzaW5nKGJvYXJkX2dhbWVzKQ0KYGBgDQoNCiMjIyBFbGltaW5hbW9zIGxhIGNvbHVtbmEgcXVlIHRpZW5lIG3DoXMgbWlzc2luZyB2YWx1ZXMNCg0KRWxpbWluYW1vcyBjb21waWxhdGlvbiB5IGV4cGFuc2lvbiBkZSBudWVzdHJvIGRhdGFmcmFtZToNCg0KYGBge3J9DQpmaW5hbF9ib2FyZF9nYW1lcyA8LSBkcm9wX2NvbHVtbnMoYm9hcmRfZ2FtZXMsIGMoImRlc2NyaXB0aW9uIiwgImltYWdlIiwgIm5hbWUiLCAidGh1bWJuYWlsIiwgImdhbWVfaWQiLCAiY29tcGlsYXRpb24iLCJleHBhbnNpb24iLCAiZmFtaWx5IiwgImFydGlzdCIsICJtZWNoYW5pYyIpKQ0KZmluYWxfYm9hcmRfZ2FtZXMgPC0gZHJvcF9jb2x1bW5zKGZpbmFsX2JvYXJkX2dhbWVzLCBjKCJkZXNpZ25lciIsICJwdWJsaXNoZXIiKSkNCmNvbG5hbWVzKGZpbmFsX2JvYXJkX2dhbWVzKQ0KYGBgDQoNCg0KYGBge3J9DQpmaW5hbF9ib2FyZF9nYW1lcyA8LSBuYS5vbWl0KGZpbmFsX2JvYXJkX2dhbWVzKSANCmBgYA0KDQojIyMgw4FuYWxpc2lzIGRlIENvcnJlbGFjacOzbg0KDQpQb2RlbW9zIHZlciBsYSBtw6FzIGFsdGEgY29ycmVsYWNpw7NuIGVuIGVzdGFzIHZhcmlhYmxlczoNCg0KLSBtaW5fcGxheXRpbWUtbWF4X3BsYXl0aW1lDQotIG1pbl9wbGF5dGltZS1taW5fYWdlDQotIG1pbl9wbGF5dGltZS1wbGF5aW5nX3RpbWUNCi0gYXZlcmFnZV9yYXRpbmctbWluX2FnZQ0KDQpgYGB7cn0NCnBsb3RfY29ycmVsYXRpb24obmEub21pdChmaW5hbF9ib2FyZF9nYW1lcyksIG1heGNhdCA9IDVMKQ0KYGBgDQpBaG9yYSBkZSB1bmEgbWFuZXJhIG3DoXMgZGV0YWxsYWRhIHZhbW9zIGEgYW5hbGl6YXIgbGFzIHZhcmlhYmxlcyBtw6FzIGNvcnJlbGFjaW9uYWRhcyBlbnRyZSBzw60uIEVsIHRvcCAxMDoNCg0KYGBge3J9DQpjb3JyX2Nyb3NzKGZpbmFsX2JvYXJkX2dhbWVzLCAjIG5hbWUgb2YgZGF0YXNldA0KICBtYXhfcHZhbHVlID0gMC4wNSwgIyBkaXNwbGF5IG9ubHkgc2lnbmlmaWNhbnQgY29ycmVsYXRpb25zIChhdCA1JSBsZXZlbCkNCiAgdG9wID0gMTAgIyBkaXNwbGF5IHRvcCAxMCBjb3VwbGVzIG9mIHZhcmlhYmxlcyAoYnkgY29ycmVsYXRpb24gY29lZmZpY2llbnQpDQopDQpgYGANCiMjIyBRUSBwbG90DQoNCkxhIGdyw6FmaWNhIFF1YW50aWxlLVF1YW50aWxlIGVzIHVuYSBmb3JtYSBkZSB2aXN1YWxpemFyIGxhIGRlc3Zpc2nDs24gZGUgdW5hIGRpc3RyaWJ1Y2nDs24gZGUgcHJvYmFiaWxpZGFkIGVzcGVjw61maWNhLg0KDQpEZXNwdcOpcyBkZSBhbmFsaXphciBlc3RvcyBncsOhZmljb3MsIGEgbWVudWRvIGVzIGJlbmVmaWNpb3NvIGFwbGljYXIgdW5hIHRyYW5zZm9ybWFjacOzbiBtYXRlbcOhdGljYSAoY29tbyBsb2dhcml0bW8pIHBhcmEgbW9kZWxvcyBjb21vIGxhIHJlZ3Jlc2nDs24gbGluZWFsLiBQYXJhIGhhY2VybG8sIHBvZGVtb3MgdXNhciBsYSBmdW5jacOzbiBwbG90X3FxLiBEZSBmb3JtYSBwcmVkZXRlcm1pbmFkYSwgc2UgY29tcGFyYSBjb24gbGEgZGlzdHJpYnVjacOzbiBub3JtYWwuDQoNCmBgYHtyfQ0KcXFfZGF0YSA8LSBmaW5hbF9ib2FyZF9nYW1lc1ssIGMoIm1pbl9wbGF5dGltZSIsICJtYXhfcGxheXRpbWUiLCAibWluX2FnZSIsICJwbGF5aW5nX3RpbWUiLCAiYXZlcmFnZV9yYXRpbmciKV0NCg0KcGxvdF9xcShxcV9kYXRhLCBzYW1wbGVkX3Jvd3MgPSAxMDAwTCkNCg0KYGBgDQpFbiBlbCBncsOhZmljbywgbGFzIGNvbHVtbmFzIHBhcmVjZW4gc2VzZ2FkYXMgZW4gYW1iYXMgY29sYXMuIEFwbGlxdWVtb3MgdW5hIHRyYW5zZm9ybWFjacOzbiBsb2dhcsOtdG1pY2Egc2ltcGxlIHkgZ3JhZmlxdWVtb3MgZGUgbnVldm8uIA0KYGBge3J9DQpsb2dfcXFfZGF0YSA8LSB1cGRhdGVfY29sdW1ucyhxcV9kYXRhLCAxOjUsIGZ1bmN0aW9uKHgpIGxvZyh4ICsgMSkpDQoNCg0KcGxvdF9xcShsb2dfcXFfZGF0YSwgc2FtcGxlZF9yb3dzID0gMTAwMEwpDQoNCmBgYA0KDQojIyMgw4FuYWxpc2lzIEV4cGxvcmF0b3JpbyBkZSBsb3MgRGF0b3MNClRlbmllbmRvIG51ZXN0cmFzIHZhcmlhYmxlcyBjb24gbWF5b3IgY29ycmVsYWNpw7NuIHZhbW9zIGEgZ3JhZmljYXJsYXMgY29uIGdlb20gcG9pbnQuLjoNCg0KLSBtaW5fcGxheXRpbWUtbWluX2FnZQ0KDQpgYGB7cn0NCmZpbmFsX2JvYXJkX2dhbWVzICU+JSAgZ2dwbG90KGFlcyh4ID0gbWluX3BsYXl0aW1lLCB5ID0gbWluX2FnZSkpICsgDQogIGdlb21fcG9pbnQoKQ0KYGBgDQoNCi0gYXZlcmFnZV9yYXRpbmctbWluX2FnZQ0KDQoNCmBgYHtyfQ0KZmluYWxfYm9hcmRfZ2FtZXMgJT4lICBnZ3Bsb3QoYWVzKHggPSBhdmVyYWdlX3JhdGluZywgeSA9IG1pbl9hZ2UpKSArIA0KICBnZW9tX3BvaW50KCkNCmBgYA0KDQotIGF2ZXJhZ2VfcmF0aW5nLXBsYXlpbmdfdGltZQ0KDQoNCmBgYHtyfQ0KZmluYWxfYm9hcmRfZ2FtZXMgJT4lICBnZ3Bsb3QoYWVzKHggPSBwbGF5aW5nX3RpbWUsIHkgPSBhdmVyYWdlX3JhdGluZykpICsgDQogIGdlb21fcG9pbnQoKQ0KYGBgDQoNCi0gdXNlcnNfcmF0ZWQtYXZlcmFnZV9yYXRpbmcNCg0KDQpgYGB7cn0NCmZpbmFsX2JvYXJkX2dhbWVzICU+JSAgZ2dwbG90KGFlcyh4ID0gdXNlcnNfcmF0ZWQsIHkgPSBhdmVyYWdlX3JhdGluZykpICsgDQogIGdlb21fcG9pbnQoKQ0KYGBgDQoNCg0KIyMjVXNpbmcgdnRyZWUgcGFyYSBleHBsb3Jhcg0KDQpVc2Ftb3MgdnRyZWUgcGFyYSBvYnNlcnZhciBsYSBjb25jZW50cmFjacOzbiBkZSBsb3MgZGF0b3MgcG9yIGVqZW1wbG8gcGFyYSBtaW5fYWdlLCBkb25kZSBsYSBtYXlvcsOtYSBkZSBsb3MgZGF0b3Mgc2UgY29uY2VudHJhbiBlbiBtaW5fYWdlIGRlIDggYcOxb3MsIDEwIGHDsW9zIHkgMTIgYcOxb3MuDQoNCmBgYHtyfQ0KdnRyZWUoZmluYWxfYm9hcmRfZ2FtZXMsICJtaW5fYWdlIikNCmBgYA0KDQpVc2Ftb3MgdnRyZWUgcGFyYSBvYnNlcnZhciBsYSBjb25jZW50cmFjacOzbiBkZSBsb3MgZGF0b3MgcG9yIGVqZW1wbG8gcGFyYSBtaW5fcGxheWVycywgdGVuZW1vcyBjYXNpIHVuIDY5JSBwYXJhIG1pbiAyIGp1Z2Fkb3JlcyB5IGNlcmNhIGRlbCAxOSUgcGFyYSBtaW4gMyBqdWdhZG9yZXMuDQoNCmBgYHtyfQ0KdnRyZWUoZmluYWxfYm9hcmRfZ2FtZXMsICJtaW5fcGxheWVycyIpDQpgYGANCg0KDQpVc2Ftb3MgdnRyZWUgcGFyYSBvYnNlcnZhciBsYSBjb25jZW50cmFjacOzbiBkZSBsb3MgZGF0b3MgcG9yIGVqZW1wbG8gcGFyYSBtYXhfcGxheWVycywgdGVuZW1vcyBjYXNpIHVuIDIzJSBwYXJhIG3DoXggNCBqdWdhZG9yZXMgeSBjZXJjYSBkZWwgMjUlIHBhcmEgbcOheCA2IGp1Z2Fkb3Jlcy4NCg0KYGBge3J9DQp2dHJlZShmaW5hbF9ib2FyZF9nYW1lcywgIm1heF9wbGF5ZXJzIikNCmBgYA0KDQoNCiMjIyDCv1F1ZSBzZSBoYSBoZWNobyBoYXN0YSBhaG9yYT8NCg0KU2UgcmVhbGl6w7MgdW5hIGV4cGxvcmFjacOzbiBkZSBkYXRvcywgZG9uZGUgcHJpbWVybyBlbGltaW5hbG9zIGNvbHVtbmFzIHF1ZSBubyB0aWVuZW4gbXVjaGEgc2lnbmlmaWNhbmNpYSBlbiBsYSBwcmVkaWNjacOzbiBkZSBudWVzdHJhIHZhcmlhYmxlIGRlIGNhbGlmaWNhY2nDs24uIERlc3B1w6lzIHZpbW9zIHN1IGNvcnJlbGFjacOzbiBlbnRyZSBsYXMgZXhpc3RlbnRlcy4NCg0KU2UgdGllbmUgbcOhcyBjbGFybyBjdWFsZXMgc29uIGxhcyB2YXJpYWJsZXMgbcOhcyBzaWduaWZpY2F0aXZhcyBhIGxhIHByZWRpY2Npw7NuLCBzZSBoaXpvIHVuYSBsaW1waWV6YSwgdGVuZW1vcyBkYXRvcyBtw6FzIGNvbnR1bmRlbnRlcyBjb24gbG9zIGN1YWxlcyBjb21lbnphciBudWVzdHJhIHByZWRpY2Npw7NuLCBtZW5vcyBvdXRsaWVycyBzb2JyZSB0b2RvLg0KDQoNCiMjIFByb3B1ZXN0YXMNCg0KRGViaWRvIGEgcXVlIGVsIHByb2JsZW1hIGludGVudGEgY29udmVuY2VyIGEgSm9zw6kgZGUgcXVlIHN1cyBqdWVnb3MgcHVkaWVyb24gaGFiZXIgc2lkbyAoZW4gcHJvbWVkaW8pIGJpZW4gcmVjaWJpZG9zLCB5IGRlIGPDs21vIHNlIGVzcGVyYSBxdWUgc2UgcmVjaWJhbiBlbiB1biBmdXR1cm8sIGxhIHZhcmlhYmxlIGRlIHNhbGlkYSBkZSBudWVzdHJvIHByb2JsZW1hIGVzIGxhIGNhbGlmaWNhY2nDs24gZGUgbG9zIHVzdWFyaW9zIGRlbCBzaXRpbyB3ZWIuIEVzdG8gcHVlZGUgaGFjZXJzZSBkZSBkb3MgbWFuZXJhczogdW5hIHJlZ3Jlc2nDs24geSB0b21hciBsYSBjYWxpZmljYWNpw7NuIGNvbW8gdW5hIHZhcmlhYmxlIGNvbnRpbnVhLCBvIHJlZG9uZGVhciB5IHRvbWFybG8gY29tbyBwcm9ibGVtYSBkZSBjbGFzaWZpY2FjacOzbiAoY2FsaWZpY2FjacOzbiBkaXNjcmV0YSBkZSAwIGEgMTApLiBMYXMgcHJvcHVlc3RhcyBwYXJhIGVzdG9zIGNhc29zIHNvbg0KDQojIyMgUmVncmVzacOzbg0KLSBTdXBwb3J0IFZlY3RvciBSZWdyZXNzaW9uDQotIFJhbmRvbSBGb3Jlc3QNCi0gUmVncmVzacOzbiBsaW5lYWwgbcO6bHRpcGxlDQoNCiMjIyBDbGFzaWZpY2FjacOzbg0KLSBTdXBwb3J0IFZlY3RvciBNYWNoaW5lDQotIFJhbmRvbSBGb3Jlc3QNCi0gTXVsdGlsYXllciBwZXJjZXB0cm9uDQoNClZhbW9zIGEgc3Vwb25lciBxdWUgYSBsYSBjb211bmlkYWQgZGUganVlZ29zIGRlIG1lc2Egbm8gbGVzIGltcG9ydGEgdGFudG8gZWwgaGlzdG9yaWFsIGRlbCBhdXRvciBkZWwganVlZ28gbmkgcXVpw6luIGxvIHB1YmxpcXVlLCBwb3IgbG8gcXVlIGVzYXMgY29sdW1uYXMgc2UgZWxpbWluYXLDrWFuIGRlbCBhbsOhbGlzaXMuDQpTaSBKb3PDqSB2ZSBxdWUgc3VzIGp1ZWdvcyBubyBodWJpZXJhbiBndXN0YWRvLCBhbCBtZW5vcyBwb2Ryw6EgdGVuZXIgdW4gbW9kZWxvIGNvbiBlbCBjdcOhbCBwdWVkZSBzYWJlciBxdcOpIGVzIGxvIHF1ZSBzdWVsZSBndXN0YXJsZSBhIGxhIGdlbnRlLCBwb3IgbG8gcXVlIHBvZHLDrWEgaGFjZXIgaW52ZXN0aWdhY2nDs24gZGUgc2VndWltaWVudG8gcGFyYSBlbnRhYmxhciBsYXMgY2F1c2FzIHJhw61jZXMuDQoNCiMgTW9kZWxhZG8NCg0KUHJpbWVybyBoYWNlbW9zIGxhIHNlcGFyYWNpw7NuIGRlIGxvcyBkYXRvcyBlbiB0cmFpbiB5IHRlc3QuIFRvZG9zIGxvcyBtb2RlbG9zIHVzYXLDoW4gbG9zIG1pc21vcyBzdWJjb25qdW50b3MgcGFyYSBwb2RlciBldmFsdWFybG9zIHkgY29tcGFyYXJsb3MgZW4gdW4gdGVycmVubyBuaXZlbGFkby4NCg0KYGBge3J9DQpsaWJyYXJ5KGNhVG9vbHMpDQpzZXQuc2VlZCgwKQ0Kc3BsaXQgPSBzYW1wbGUuc3BsaXQoZmluYWxfYm9hcmRfZ2FtZXMsIFNwbGl0UmF0aW89MC42KQ0KZGF0YS50cmFpbiA9IHN1YnNldChmaW5hbF9ib2FyZF9nYW1lcywgc3BsaXQ9VFJVRSkNCmRhdGEudGVzdCA9IHN1YnNldChmaW5hbF9ib2FyZF9nYW1lcywgc3BsaXQ9RkFMU0UpDQpgYGANCg0KDQoNCiMjIFN1cHBvcnQgVmVjdG9yIFJlZ3Jlc3Npb24NCg0KYGBge3J9DQpsaWJyYXJ5KGNhcmV0KQ0KbGlicmFyeShkb1BhcmFsbGVsKQ0Kc2V0LnNlZWQoMCkNCmNvbnRyb2wgPSB0cmFpbkNvbnRyb2wobWV0aG9kPSJyZXBlYXRlZGN2IiwgcmVwZWF0cz01LCBzZWFyY2g9InJhbmRvbSIpDQpyZWdpc3RlckRvUGFyYWxsZWwoY29yZXMgPSBwYXJhbGxlbDo6ZGV0ZWN0Q29yZXMoKSAtIDEpDQptb2RlbC5zdnIgPSB0cmFpbihhdmVyYWdlX3JhdGluZyB+IC4sIGRhdGEgPSBkcm9wX2NvbHVtbnMoZGF0YS50cmFpbiwgImNhdGVnb3J5IiksDQogICAgICAgICAgICAgICBtZXRob2QgPSAic3ZtUmFkaWFsIiwNCiAgICAgICAgICAgICAgIHR1bmVMZW5ndGggPSAxNSwNCiAgICAgICAgICAgICAgIG1ldHJpYyA9ICJSTVNFIiwNCiAgICAgICAgICAgICAgIHByZVByb2MgPSBjKCJjZW50ZXIiLCAic2NhbGUiKSwNCiAgICAgICAgICAgICAgIHRyQ29udHJvbCA9IGNvbnRyb2wpDQptb2RlbC5zdnINCg0KYGBgDQoNCmBgYHtyfQ0KcGxvdF9xcShwcmVkaWN0KG1vZGVsLnN2ciwgbmV3ZGF0YT1kYXRhLnRlc3QpIC0gZGF0YS50ZXN0JGF2ZXJhZ2VfcmF0aW5nKQ0KYGBgDQoNCiMjIEgyTyBNb2RlbHMNCg0KIyMjIEluaWNpYWxpemFyIEgyTw0KQ3JlYW1vcyBlbCBjbHVzZ3RlciBsb2NhbCBjb24gdG9kb3MgbG9zIGNvcmVzIGRpc3BvbmlibGVzIGRlIGxhIHNpZ3VpZW50ZSBmb3JtYToNClNlIGVsaW1pbmFuIGxvcyBkYXRvcyBkZWwgY2x1c3RlciBwb3Igc2kgeWEgaGFiw61hIHNpZG8gaW5pY2lhbGl6YWRvLg0KVHJhcyBpbmljaWFyIGVsIGNsdXN0ZXIgKGxvY2FsKSwgc2UgbXVlc3RyYW4gcG9yIHBhbnRhbGxhIHN1cyBjYXJhY3RlcsOtc3RpY2FzLCBlbnRyZSBsYXMgcXVlIGVzdMOhbjogZWwgbsO6bWVybyBkZSBjb3JlcyBhY3RpdmFkb3MgKDQpLCBsYSBtZW1vcmlhIHRvdGFsIGRlbCBjbHVzdGVyICg1LjMyIEdCKSwgZWwgbsO6bWVybyBkZSBub2RvcyAoMSBwb3JxdWUgc2UgZXN0w6EgZW1wbGVhbmRvIHVuIMO6bmljbyBjb21wdXRhZG9yKSB5IGVsIHB1ZXJ0byBjb24gZWwgcXVlIGNvbmVjdGFyc2UgYSBsYSBpbnRlcmZheiB3ZWIgZGUgSDJPIChodHRwOi8vbG9jYWxob3N0OjU0MzIxL2Zsb3cvaW5kZXguaHRtbCkuDQpgYGB7cn0NCiMgaW5pY2lhbGl6YWNpw7NuIGRlIGgybw0KaDJvLmluaXQoDQogIGlwID0gImxvY2FsaG9zdCIsDQogICMgLTEgaW5kaWNhIHF1ZSBzZSBlbXBsZWVuIHRvZG9zIGxvcyBjb3JlcyBkaXNwb25pYmxlcy4NCiAgbnRocmVhZHMgPSAtMSwNCiAgIyBNw6F4aW1hIG1lbW9yaWEgZGlzcG9uaWJsZSBwYXJhIGVsIGNsdXN0ZXIuDQogIG1heF9tZW1fc2l6ZSA9ICI2ZyINCikNCg0KaDJvLnJlbW92ZUFsbCgpDQpoMm8ubm9fcHJvZ3Jlc3MoKQ0KYGBgDQoNCiMjIyBDYXJnYSBkZSBkYXRvcyAtU2VwYXJhY2nDs24gZGUgdHJhaW5pbmcsIHZhbGlkYWNpw7NuIHkgdGVzdA0KTGEgY2FyZ2EgZGUgZGF0b3MgcHVlZGUgaGFjZXJzZSBkaXJlY3RhbWVudGUgYWwgY2x1c3RlciBIMk8sIG8gYmllbiBjYXJnw6FuZG9sb3MgcHJpbWVybyBlbiBtZW1vcmlhIGVuIGxhIHNlc2nDs24gZGUgUiB5IGRlc3B1w6lzIHRyYW5zZmlyacOpbmRvbG9zLiBMYSBzZWd1bmRhIG9wY2nDs24gbm8gZXMgYWNvbnNlamFibGUgc2kgZWwgdm9sdW1lbiBkZSBkYXRvcyBlcyBtdXkgZ3JhbmRlLg0KDQpQYXJhIG51ZXN0cm8gY2FzbyBlbCBjb25qdW50byBkZSBkYXRvcyBkZSB0dXJiaW5lcyBlcyBzdWZpY2llbnRlbWVudGUgcGVxdWXDsW8geSBsbyBwb2RlbW9zIGFsbWFjZW5hciBlbiBtZW1vcmlhLCBwb3IgdGFudG8gbG8gcG9kZW1vcyBsbGFtYXIgY29uIGxhIHNpZ3VpZW50ZSBmdW5jacOzbi4NCg0KQW50ZXMgZGUgaGFjZXIgbGEgc2VwYXJhY2nDs24gdGVuZ2Ftb3MgY2xhcm8gbGEgZGlmZXJlbmNpYSBlbnRyZSBlc3RhcyBwYXJ0aWNpb25lcyBkZWwgY29uanVudG8gZGUgZGF0b3M6DQoNCkRhdG9zIGRlIHRyYWluOiBsYSBtdWVzdHJhIGRlIGxvcyBkYXRvcyB1dGlsaXphZGEgcGFyYSBhanVzdGFyIGVsIG1vZGVsby4NCg0KRGF0b3MgZGUgdmFsaWRhY2nDs246IGxhIG11ZXN0cmEgZGUgZGF0b3MgcXVlIHNlIHV0aWxpemEgcGFyYSBwcm9wb3JjaW9uYXIgdW5hIGV2YWx1YWNpw7NuIGltcGFyY2lhbCBkZSB1biBhanVzdGUgZGUgbW9kZWxvIGVuIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIHRyYWluIG1pZW50cmFzIHNlIGFqdXN0YW4gbG9zIGhpcGVycGFyw6FtZXRyb3MgZGVsIG1vZGVsby4gTGEgZXZhbHVhY2nDs24gc2UgdnVlbHZlIG3DoXMgc2VzZ2FkYSBhIG1lZGlkYSBxdWUgbGEgaGFiaWxpZGFkIGRlbCBjb25qdW50byBkZSBkYXRvcyBkZSB2YWxpZGFjacOzbiBzZSBpbmNvcnBvcmEgYSBsYSBjb25maWd1cmFjacOzbiBkZWwgbW9kZWxvLg0KDQpEYXRvcyBkZSB0ZXN0OiBsYSBtdWVzdHJhIGRlIGRhdG9zIHV0aWxpemFkYSBwYXJhIHByb3BvcmNpb25hciB1bmEgZXZhbHVhY2nDs24gaW1wYXJjaWFsIGRlIHVuIGFqdXN0ZSBmaW5hbCBkZWwgbW9kZWxvIGVuIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIGVudHJlbmFtaWVudG8uDQoNCkxhIGZ1bmNpw7NuIGgyby5zcGxpdEZyYW1lKCkgcmVhbGl6YSBwYXJ0aWNpb25lcyBhbGVhdG9yaWFzLCBwZXJvIG5vIHBlcm1pdGUgaGFjZXJsYXMgZGUgZm9ybWEgZXN0cmF0aWZpY2FkYSwgcG9yIGxvIHF1ZSBubyBhc2VndXJhIHF1ZSBsYSBkaXN0cmlidWNpw7NuIGRlIGNsYXNlcyBkZSB2YXJpYWJsZSByZXNwdWVzdGEgc2VhIGlndWFsIGVuIHRvZGFzIHBhcnRpY2lvbmVzLiBFc3RvIHB1ZWRlIHNlciBwcm9ibGVtw6F0aWNvIGNvbiBkYXRvcyBtdXkgZGVzYmFsYW5jZWFkb3MgKGFsZ3VubyBkZSBsb3MgZ3J1cG9zIGVzIG11eSBtaW5vcml0YXJpbykuDQoNCg0KRW4gZWwgbW9tZW50byBlbiBxdWUgY29uc2lkZXJlbW9zIGxhIHZhbGlkYWNpw7NuLCBkZWJlbW9zIGFncmVnYXIgZW4gbG9zIHJhdGlvcyBlbCBwb3JjZW50YWplIGRlIGxhIHZhbGlkYWNpw7NuLCBlbiBlc3RlIGNhc28gc2Vyw6EgdHJhaW4gKDYwJSksIHZhbGlkYWNpw7NuICgyMCUpIHkgdGVzdCAoMjAlKS4gRW4gbGEgc2VtaWxsYSBzZSBsZSBhZ3JlZ2EgZWwgZWwgbnVtZXJhbCA0IHkgc2UgYWRpY2lvbmEgdW4gbnVldm8gc3ViY29uanVudG8gZGUgZGF0b3MsIGVudGVuZGllbmRvIHF1ZSBlbCAxIGVzIHRyYWluLCBlbCAyIGVzIHZhbGlkYWNpw7NuIHkgZWwgMyBlcyB0ZXN0Lg0KDQoNCg0KYGBge3J9DQpkYXRvc19oMm8gPC0gYXMuaDJvKHggPSBmaW5hbF9ib2FyZF9nYW1lcywgZGVzdGluYXRpb25fZnJhbWUgPSAiZGF0b3NfaDJvIikNCg0KZGF0b3NfdHJhaW5faDJvIDwtIGFzLmgybyh4ID0gZGF0YS50cmFpbiwga2V5ID0gImRhdG9zX3RyYWluX2gybyIpDQpkYXRvc192YWxpZF9oMm8gPC0gYXMuaDJvKHggPSBkYXRhLnRlc3QsIGtleSA9ICJkYXRvc192YWxpZF9oMk8iKQ0KYGBgDQoNCiMjIyBSYW5kb20gRm9yZXN0DQoNCkxhIGZ1bmNpw7NuIHBhcmEgZXN0ZSBtb2RlbG8gZW4gaDJvIGVzIGgyby5yYW5kb21Gb3Jlc3QuIERlbnRybyBkZSBlbGxhIGRlYmVtb3MgZGUgZXNwZWNpZmljYXIgbG9zIGRhdG9zIGRlIHRyYWluIHF1ZSBjb252ZXJ0aW1vcyBkZW50cm8gZGUgaDJvIHksIHNpIGFzw60gbG8gcXVlcmVtb3MgbG9zIGRhdG9zIGRlIHZhbGlkYWNpw7NuLiBQYXJhIGN1YW5kbyBubyBxdWVyZW1vcyB1dGlsaXphciBkYXRvcyBkZSB2YWxpZGFjacOzbiBlc3RhIGzDrW5lYSBzZSBvbWl0ZSBkZW50cm8gZGVsIG1vZGVsbyBjYW1iaWEgbGEgcGFydGljacOzbiBkZWwgY29uanVudG8gZGUgZGF0b3MuIFNlIGRlc2NhcnRhbiBsYXMgY29sdW1uYXMgY2F0ZWfDs3JpY2FzICwgdXNhbW9zIHNvbG8gbGFzIG7Dum1lcmljYXMgcGFyYSBlc3RlIHJhbmRvbSBmb3Jlc3QsIHRhbWJpw6luIHF1aXRhbW9zIGVsIG9iamVjdF9pZCwgc29sbyBub3MgaW50ZXJlc2EgZWwgcmFuZ28geCA9IGMoMSwgMiwgMywgNCwgNSwgNiwgNywgOCwgMTApLCB5IHN5IHByZWRpY2Npw7NuIHF1ZSBlcyBsYSB5ID0gOS4NCg0KYGBge3J9DQptb2RlbC5oMm8ucmYgPSBoMm8ucmFuZG9tRm9yZXN0KA0KICB0cmFpbmluZ19mcmFtZSA9IGRhdG9zX3RyYWluX2gybywNCiAgdmFsaWRhdGlvbl9mcmFtZSA9IGRhdG9zX3ZhbGlkX2gybywNCiAgeCA9IGMoMSwgMiwgMywgNCwgNSwgNiwgNywgOCwgMTApLA0KICB5ID0gOSwNCiAgbW9kZWxfaWQgPSAicmZfY292VHlwZV92MSIsDQogIG50cmVlcyA9IDIwMCwNCiAgc3RvcHBpbmdfcm91bmRzID0gMiwNCiAgc2NvcmVfZWFjaF9pdGVyYXRpb24gPSBULA0KICBzZWVkID0gMjYNCikNCg0Kc3VtbWFyeShtb2RlbC5oMm8ucmYpDQpgYGANCg0KIyMjIEdyYWRpZW50IEJvb3N0aW5nIE1hY2hpbmVzIChHQk0pDQpQcmltZXJvIGhhcmVtb3MgdG9kYXMgbGEgY29uZmlndXJhY2lvbmVzIHByZWRldGVybWluYWRhcyB5IGx1ZWdvIGNvbWVuemFyZW1vcyBhIGhhY2VyIGFsZ3Vub3MgY2FtYmlvcyBkb25kZSBzZSBkZXNjcmliZW4gbG9zIHBhcsOhbWV0cm9zIHkgbG9zIHZhbG9yZXMgcHJlZGV0ZXJtaW5hZG9zLg0KDQpQb2RlbW9zIG9ic2VydmFyIHVuYSBlc3RydWN0dXJhIG11eSBzaW1pbGFyIGEgbGEgZGVsIHJhbmRvbSBmb3Jlc3QsIGFob3JhIHV0aWxpemFyZW1vcyBsYSBmdW5jacOzbiBoMm8uZ2JtLi4gTk9UQTogRW4gbGEgbWF5b3LDrWEgZGUgbG9zIGFsZ29yaW1vcyBlbCBwcmltZXJvIGVzIHBhcmEgcmVncmVzacOzbiB5IGVsIHNlZ3VuZG8gcGFyYSBjbGFzaWZpY2FjacOzbi4NCg0KYGBge3J9DQpnYm1fbW9kZWwgPC0gaDJvLmdibSgNCiAgdHJhaW5pbmdfZnJhbWUgPSBkYXRvc190cmFpbl9oMm8sICMgZGF0b3MgZGUgaDJvIHBhcmEgdHJhaW5pbmcNCiAgdmFsaWRhdGlvbl9mcmFtZSA9IGRhdG9zX3ZhbGlkX2gybywgIyBkYXRvcyBkZSBoMm8gcGFyYSB2YWxpZGFjacOzbiAobm8gZXMgcmVxdWVyaWRvKQ0KICB4ID0gYygxLCAyLCAzLCA0LCA1LCA2LCA3LCA4LCAxMCksLCAjIExhcyBjb2x1bW5hcyBwcmVkaWN0b3JhcywgcG9yIMOtbmRpY2UNCiAgeSA9IDksICAgICMgTGEgY29sdW1uYSBxdWUgcXVlcmVtb3MgcHJlZGVjaXIsIHZhcmlhYmxlIG9iamV0aXZvDQogIG1vZGVsX2lkID0gImdibV9jb3ZUeXBlMSIsICMgbm9tYnJlIGRlbCBtb2RlbG8gZW4gaDJvDQogIHNlZWQgPSAyMDAwMDAwICAgIyBFc3RhYmxlY2VyIHVuYSBzZW1pbGxhIGFsZWF0b3JpYSBwYXJhIHF1ZSBzZSBwdWVkYSByZXByb2R1Y2lyDQopIA0KDQpzdW1tYXJ5KGdibV9tb2RlbCkNCmBgYA0KDQojIyMjIFNjb3JpbmcgZGVsIG1vZGVsbw0KUG9kZW1vcyB2ZXIgbGEgZXZvbHVjacOzbiBkZWwgbW9kZWxvLCBwYXJhIGV2YWx1YXIgY8OzbW8gYXByZW5kZSBlbCBtb2RlbG8gYSBtZWRpZGEgcXVlIHNlIGHDsWFkZW4gbnVldm9zIMOhcmJvbGVzIGFsIGVuc2FtYmxlLg0KDQpoMm8gYWxtYWNlbmEgbGFzIG3DqXRyaWNhcyBkZSBlbnRyZW5hbWllbnRvIHkgdGVzdCBiYWpvIGVsIG5vbWJyZSBkZSBzY29yaW5nLiBMb3MgdmFsb3JlcyBzZSBlbmN1ZW50cmFuIGFsbWFjZW5hZG9zIGRlbnRybyBkZWwgbW9kZWxvLg0KYGBge3J9DQpzY29yaW5nIDwtIGFzLmRhdGEuZnJhbWUoZ2JtX21vZGVsQG1vZGVsJHNjb3JpbmdfaGlzdG9yeSkNCmhlYWQoc2NvcmluZykNCmBgYA0KIyMjIyBJbXBvcnRhbmNpYSBWYXJpYWJsZXMgZGVsIG1vZGVsbw0KDQpFbiBsb3MgbW9kZWxvcyBHQk0sIHNlIHB1ZWRlIGVzdHVkaWFyIGxhIGluZmx1ZW5jaWEgZGUgbG9zIHByZWRpY3RvcmVzIGN1YW50aWZpY2FuZG8gbGEgcmVkdWNjacOzbiB0b3RhbCBkZSBlcnJvciBjdWFkcsOhdGljbyBxdWUgaGEgY29uc2VndWlkbyBjYWRhIHByZWRpY3RvciBlbiBlbCBjb25qdW50byBkZSB0b2RvcyBsb3Mgw6FyYm9sZXMgcXVlIGZvcm1hbiBlbCBtb2RlbG8uDQpgYGB7cn0NCmltcG9ydGFuY2lhIDwtIGFzLmRhdGEuZnJhbWUoZ2JtX21vZGVsQG1vZGVsJHZhcmlhYmxlX2ltcG9ydGFuY2VzKQ0KaW1wb3J0YW5jaWENCmBgYA0KDQojIyMjIGdncGxvdCB2YXJpYWJsZXMgaW1wb3J0YW5jaWEgZGVsIG1vZGVsbw0KYGBge3J9DQpnZ3Bsb3QoZGF0YSA9IGltcG9ydGFuY2lhLA0KICAgICAgIGFlcyh4ID0gcmVvcmRlcih2YXJpYWJsZSwgc2NhbGVkX2ltcG9ydGFuY2UpLCB5ID0gc2NhbGVkX2ltcG9ydGFuY2UpKSArDQogIGdlb21fY29sKCkgKw0KICBjb29yZF9mbGlwKCkgKw0KICBsYWJzKHRpdGxlID0gIkltcG9ydGFuY2lhIGRlIGxvcyBwcmVkaWN0b3JlcyBlbiBlbCBtb2RlbG8gR0JNIiwNCiAgICAgICBzdWJ0aXRsZSA9ICJJbXBvcnRhbmNpYSBlbiBiYXNlIGEgbGEgcmVkdWNjacOzbiBkZWwgZXJyb3IgY3VhZHLDoXRpY28gbWVkaW8iLA0KICAgICAgIHggPSAiUHJlZGljdG9yIiwNCiAgICAgICB5ID0gIkltcG9ydGFuY2lhIHJlbGF0aXZhIikgKw0KICB0aGVtZV9idygpDQpgYGANCg0KIyMjIE1vZGVsbyBHQk0gYWx0ZXJuYXRpdm8NCg0KRW4gbG9zIG1vZGVsb3MgR0JNLCBzZSBwdWVkZSBlc3R1ZGlhciBsYSBpbmZsdWVuY2lhIGRlIGxvcyBwcmVkaWN0b3JlcyBjdWFudGlmaWNhbmRvIGxhIHJlZHVjY2nDs24gdG90YWwgZGUgZXJyb3IgY3VhZHLDoXRpY28gcXVlIGhhIGNvbnNlZ3VpZG8gY2FkYSBwcmVkaWN0b3IgZW4gZWwgY29uanVudG8gZGUgdG9kb3MgbG9zIMOhcmJvbGVzIHF1ZSBmb3JtYW4gZWwgbW9kZWxvLg0KYGBge3J9DQpnYm1fbW9kZWxfMiA8LSBoMm8uZ2JtKA0KICB0cmFpbmluZ19mcmFtZSA9IGRhdG9zX3RyYWluX2gybywgIyBkYXRvcyBkZSBoMm8gcGFyYSB0cmFpbmluZw0KICB2YWxpZGF0aW9uX2ZyYW1lID0gZGF0b3NfdmFsaWRfaDJvLCAjIGRhdG9zIGRlIGgybyBwYXJhIHZhbGlkYWNpw7NuIChubyBlcyByZXF1ZXJpZG8pDQogIHggPSBjKDI6Myw1OjExKSwgIyBMYXMgY29sdW1uYXMgcHJlZGljdG9yYXMsIHBvciDDrW5kaWNlDQogIHkgPSA0LCAgICAjIExhIGNvbHVtbmEgcXVlIHF1ZXJlbW9zIHByZWRlY2lyLCB2YXJpYWJsZSBvYmpldGl2bw0KICBtb2RlbF9pZCA9ICJnYm1fY292VHlwZTEiLCAjIG5vbWJyZSBkZWwgbW9kZWxvIGVuIGgybw0KICBudHJlZXMgPSAyMDAsIA0KICBtYXhfZGVwdGggPSAzMCwNCiAgc3RvcHBpbmdfcm91bmRzID0gMiwNCiAgc3RvcHBpbmdfdG9sZXJhbmNlID0gMWUtMiwNCiAgc2VlZCA9IDIwMDAwMDAgICAjIEVzdGFibGVjZXIgdW5hIHNlbWlsbGEgYWxlYXRvcmlhIHBhcmEgcXVlIHNlIHB1ZWRhIHJlcHJvZHVjaXINCikgDQpgYGANCg0KIyMjIyBNw6l0cmljYXMNCmBgYHtyfQ0KZ2JtX21vZGVsXzJAbW9kZWwkdmFsaWRhdGlvbl9tZXRyaWNzDQpgYGANCg0KIyMjIyBQcmVkaWNjaW9uZXMgeSBlcnJvcg0KVW5hIHZleiBoZW1vcyBhanVzdGFkbyBlbCBtb2RlbG8sIHNlIHB1ZWRlIHByZWRlY2lyIG51ZXZhcyBvYnNlcnZhY2lvbmVzIHkgZXN0aW1hciBlbCBlcnJvciBkZSB0ZXN0Lg0KDQpgYGB7cn0NCiMgUHJlZGljdG9yZXMgcGFyYSBlbCBtb2RlbG8gZGUgcmFuZG9tIGZvcmVzdA0KcHJlZGljY2lvbmVzIDwtIGgyby5wcmVkaWN0KA0KICBvYmplY3QgPSBtb2RlbC5oMm8ucmYsDQogIG5ld2RhdGEgPSBkYXRvc192YWxpZF9oMm8NCikNCmhlYWQocHJlZGljY2lvbmVzKQ0KYGBgDQoNCmBgYHtyfQ0KIyBQcmVkaWN0b3JlcyBwYXJhIGVsIG1vZGVsbyBkZSBHQk0NCnByZWRpY2Npb25lc18yIDwtIGgyby5wcmVkaWN0KA0KICBvYmplY3QgPSBnYm1fbW9kZWwsDQogIG5ld2RhdGEgPSBkYXRvc192YWxpZF9oMm8NCikNCmhlYWQocHJlZGljY2lvbmVzXzIpDQpgYGANCg0KDQojIENvbXBhcmFzacOzbg0KDQpFbiB0b3RhbCBzZSB0dXZpZXJvbiA0IG1vZGVsb3M6IFN1cHBvcnQgVmVjdG9yIFJlZ3Jlc3Npb24sIFJhbmRvbSBGb3Jlc3QsIEdyYWRpZW50IEJvb3N0aW5nIE1hY2hpbmUgeSB1biBHQk0gYWx0ZXJuYXRpdm8uIFJldmlzZW1vcyBzdXMgZXJyb3JlcyBkZSBlbnRyZW5hbWllbnRvIHkgZGUgcHJ1ZWJhLg0KDQpgYGB7cn0NCnN2ci5ybXNlLnRyYWluID0gbWluKG1vZGVsLnN2ciRyZXN1bHRzJFJNU0UpDQpzdnIucm1zZS50ZXN0ID0gTW9kZWxNZXRyaWNzOjpybXNlKHByZWRpY3QobW9kZWwuc3ZyLCBuZXdkYXRhPWRhdGEudGVzdCksIGRhdGEudGVzdCRhdmVyYWdlX3JhdGluZykNCg0KcmYucm1zZS50cmFpbiA9IHRhaWwobW9kZWwuaDJvLnJmQG1vZGVsJHNjb3JpbmdfaGlzdG9yeSR0cmFpbmluZ19ybXNlLCAxKQ0KcmYucm1zZS50ZXN0ID0gdGFpbChtb2RlbC5oMm8ucmZAbW9kZWwkc2NvcmluZ19oaXN0b3J5JHZhbGlkYXRpb25fcm1zZSwgMSkNCg0KZ2JtMS5ybXNlLnRyYWluID0gdGFpbChnYm1fbW9kZWxAbW9kZWwkc2NvcmluZ19oaXN0b3J5JHRyYWluaW5nX3Jtc2UsIDEpDQpnYm0xLnJtc2UudGVzdCA9IHRhaWwoZ2JtX21vZGVsQG1vZGVsJHNjb3JpbmdfaGlzdG9yeSR2YWxpZGF0aW9uX3Jtc2UsIDEpDQoNCmdibTIucm1zZS50cmFpbiA9IHRhaWwoZ2JtX21vZGVsXzJAbW9kZWwkc2NvcmluZ19oaXN0b3J5JHRyYWluaW5nX3Jtc2UsIDEpDQpnYm0yLnJtc2UudGVzdCA9IHRhaWwoZ2JtX21vZGVsXzJAbW9kZWwkc2NvcmluZ19oaXN0b3J5JHZhbGlkYXRpb25fcm1zZSwgMSkNCmBgYA0KDQpEZSBpenF1aWVyZGEgYSBkZXJlY2hhOiBSTVNFIGRlIGVudHJlbmFtaWVudG8gZGUgU1ZSLCBSRiwgR0JNMSB5IEdCTTINCg0KYGBge3J9DQpiYXJwbG90KGMoc3ZyLnJtc2UudHJhaW4sIHJmLnJtc2UudHJhaW4sIGdibTEucm1zZS50cmFpbiwgZ2JtMi5ybXNlLnRyYWluKSkNCmBgYA0KDQpEZSBpenF1aWVyZGEgYSBkZXJlY2hhOiBSTVNFIGRlIHZhbGlkYWNpw7NuIGRlIFNWUiwgUkYsIEdCTTEgeSBHQk0yDQoNCmBgYHtyfQ0KYmFycGxvdChjKHN2ci5ybXNlLnRlc3QsIHJmLnJtc2UudGVzdCwgZ2JtMS5ybXNlLnRlc3QsIGdibTIucm1zZS50ZXN0KSkNCmBgYA0KDQpSZWNvcmRlbW9zIHF1ZSB0b2RvcyBsb3MgbW9kZWxvcyB1c2FuIGVsIG1pc21vIHN1YmNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8geSBkZSB2YWxpZGFjacOzbiwgeSB0b2RvcyBwcmVzZW50YW4gdW5hIHJldHJvYWxpbWVudGFjacOzbiBwYXJhIGxhIG9wdGltaXphY2nDs24gZGUgaGlwZXJwYXLDoW1ldHJvcy4NClBhcmVjZSBxdWUgZWwgbWVqb3IgbW9kZWxvIGVzIGVsIEdCTTIsIHB1ZXMgdGllbmUgdW4gZXJyb3IgZGUgZW50cmVuYW1pZW50byB5IGRlIHZhbGlkYWNpw7NuIG11Y2hvIG3DoXMgYmFqb3MgcXVlIGxvcyBvdHJvcy4NCg0KIyBDb25jbHVzaW9uZXMNCg0KTGEgZXhwbG9yYWNpw7NuIGRlIGRhdG9zIGVzIHVuYSBmYXNlIG11eSBpbXBvcnRhbnRlIGVuIGVsIGNpY2xvIGRlIHZpZGEgZGUgdW4gcHJveWVjdG8gZGUgY2llbmNpYSBkZSBkYXRvcy4gRWwgZW50ZW5kZXIgbGEgZGlzdHJpYnVjacOzbiBkZSBsYXMgdmFyaWFibGVzIHRlIGRhIHVuYSBpZGVhIG11Y2hvIG3DoXMgY2xhcmEgZGUgcXXDqSBlcyBsbyBxdWUgcG9kcsOtYXMgdXNhciBwYXJhIHByZWRlY2lyIGxhIHNhbGlkYSBxdWUgc2UgbmVjZXNpdGE7IGF1bnF1ZSBlbCBlbnRlbmRpbWllbnRvIGRlbCBuZWdvY2lvIGVzIHVuYSBmYXNlIHF1ZSBwdWVkZSB0b21hciB1biB0aWVtcG8gbcOhcyBsYXJnbyAobm9zb3Ryb3MgdHV2aW1vcyBsYSBzdWVydGUgZGUgcXVlIHlhIGVudGVuZMOtYW1vcyBjw7NtbyBmdW5jaW9uYWJhIGVsIHNpdGlvIHdlYiBlbiBlbCBxdWUgc2UgYmFzYSBlbCBjb25qdW50byBkZSBkYXRvcyBxdWUgdXNhbW9zKS4NCg0KTGFzIG1pbCB5IHVuYSBmb3JtYXMgZGUgaW1wbGVtZW50YXIgdW4gbW9kZWxvIHByZWRpY3Rpdm8gdGFtYmnDqW4gc2UgY29udmllcnRlbiBlbiB1bmEgYmFycmVyYSBwYXJhIHNlZ3VpciBlbCBwcm95ZWN0bzogwr9jdcOhbCBkZSB0b2RhcyBsYXMgb3BjaW9uZXMgZXMgbGEgbWVqb3IgcGFyYSBlbCBwcm9ibGVtYSBxdWUgc2UgdGllbmU/IMK/Q8OzbW8ganVzdGlmaWNhcyB1c2FyIHVuIFJhbmRvbSBGb3Jlc3QgY29udHJhIHVuYSByZWQgbmV1cm9uYWw/IChTZWd1cmFtZW50ZSBjb24gcHLDoWN0aWNhIHkgcGVyaWNpYSkuDQoNCiMgUmVmZXJlbmNpYXMNCg0KKiBKb2hhbiBBIEsgU3V5a2VucywgVG9ueSBWYW4gR2VzdGVsLCBKb3MgRGUgQnJhYmFudGVyLCBCYXJ0RGUgTW9vciwgYW5kIEpvb3MgVmFuZGV3YWxsZS5MZWFzdCBTcXVhcmVzIFN1cHBvcnQgVmVjdG9yTWFjaGluZXMuIFdvcmxkIFNjaWVudGlmaWMsMjAwMi4gSVNCTjk3ODk4MTIzODE1MTQuIFVSTGh0dHBzOi8vd3d3Lndvcmxkc2NpZW50aWZpYy5jb20vd29ybGRzY2lib29rcy8xMC4xMTQyLzUwODkuDQoNCiogaHR0cDovL2RvY3MuaDJvLmFpL2gyby10dXRvcmlhbHMvbGF0ZXN0LXN0YWJsZS9pbmRleC5odG1sDQoNCiogaHR0cHM6Ly9kb2NzLmgyby5haS9oMm8vbGF0ZXN0LXN0YWJsZS9oMm8tci9kb2NzL3JlZmVyZW5jZS9oMm8ucmFuZG9tRm9yZXN0Lmh0bWwNCg0KKiBKb2FxdcOtbiBBbWF0IFJvZHJpZ28sIE1hY2hpbmUgTGVhcm5pbmcgY29uIEgyTyB5IFIuIEFicmlsIDIwMjAuIGh0dHBzOi8vcnB1YnMuY29tL0pvYXF1aW5fQVIvNDA2NDgwDQoNCg0K